package org.altervista.cp.thinjdbc.ds;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;

import javax.sql.DataSource;

import org.altervista.cp.thinjdbc.util.LogHelper;

public class ValidatingDataSource extends PoolingDataSource {

	private static final LogHelper logger = new LogHelper(
			ValidatingDataSource.class);

	public static final int DEFAULT_VALIDATE_MILLIS_INTERVAL = 10;

	public ValidatingDataSource(DataSource ds, String validatingSQL,
			long validateMillisInterval) {
		super();

		_ds = ds;
		defaultInit(validatingSQL, validateMillisInterval);
	}

	public Connection getConnection(boolean forceValidation) {
		return getConnection(forceValidation, null, null);
	}

	public Connection getConnection(boolean forceValidation, String username,
			String password) {
		return pooledConnection(forceValidation, username, password);
	}

	private void defaultInit(String validatingSQL, long validateMillisInterval) {
		_validatingSQL = validatingSQL;
		_validateMillisInterval = validateMillisInterval;

		boolean validate = false;
		Connection c = null;
		try {
			c = newConnection(null, null);
			validate = validate(c, true);
		} catch (SQLException e) {
			validate = true;
		} finally {
			if (c != null) {
				try {
					c.close();
				} catch (SQLException e) {
				}
			}
		}
		if (!validate) {
			throw new IllegalArgumentException(
					"Error in executing the validationg SQL statement ("
							+ validatingSQL + ")");
		}
	}

	private Connection newConnection(String username, String password)
			throws SQLException {
		Connection c = null;
		if (username == null) {
			c = _ds.getConnection();
		} else {
			c = _ds.getConnection(username, password);
		}
		return c;
	}

	private Connection pooledConnection(boolean forceValidation,
			String username, String password) {
		Connection c = null;
		for (int i = 0; i < _POOL.getMaxSize(); i++) {
			c = _POOL.poll(username, password);
			if (validate(c, forceValidation)) {
				break;
			} else {
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
				}
				_POOL.discard();
			}
		}
		return c;
	}

	private boolean validate(Connection c, boolean forceValidation) {
		synchronized (_TIMESTAMP_CACHE) {
			boolean valid = false;
			int key = hashCode();
			Long timestamp = _TIMESTAMP_CACHE.get(key);
			if (!forceValidation
					&& (timestamp == null || System.currentTimeMillis()
							- timestamp < _validateMillisInterval)) {
				_TIMESTAMP_CACHE.put(key, System.currentTimeMillis());
				valid = true;
			} else {
				valid = validate(c);
				if (valid) {
					_TIMESTAMP_CACHE.put(key, System.currentTimeMillis());
				} else {
					logger.warn("Discarding an invalid connection from pool");
					_TIMESTAMP_CACHE.remove(key);
					try {
						getConnectionPool().shutdown(c);
					} catch (SQLException ex) {
					}
				}
			}
			return valid;
		}
	}

	private boolean validate(Connection c) {
		Statement stm = null;
		try {
			stm = c.createStatement();
			stm.executeQuery(_validatingSQL);
			return true;
		} catch (SQLException e) {
			e.printStackTrace();
			return false;
		} finally {
			if (stm != null) {
				try {
					stm.close();
				} catch (SQLException e) {
				}
			}
		}
	}

	private final HashMap<Integer, Long> _TIMESTAMP_CACHE = new HashMap<Integer, Long>();

	private DataSource _ds;
	private String _validatingSQL;
	private long _validateMillisInterval;

	protected ConnectionPool getConnectionPool() {
		return new ConnectionPool() {

			public Connection newElement(Object... args) {
				try {
					return newConnection((String) args[0], (String) args[1]);
				} catch (SQLException e) {
					throw new IllegalStateException(e);
				}
			}

			protected void shutdown(Connection c) throws SQLException {
				c.close();
			}
		};
	}

	public PrintWriter getLogWriter() throws SQLException {
		return _ds.getLogWriter();
	}

	public int getLoginTimeout() throws SQLException {
		return _ds.getLoginTimeout();
	}

	public void setLogWriter(PrintWriter out) throws SQLException {
		_ds.setLogWriter(out);
	}

	public void setLoginTimeout(int seconds) throws SQLException {
		_ds.setLoginTimeout(seconds);
	}

	public boolean isWrapperFor(Class<?> iface) throws SQLException {
		return DataSource.class.isAssignableFrom(iface);
	}

	@SuppressWarnings("unchecked")
	public <T> T unwrap(Class<T> iface) throws SQLException {
		return (T) _ds;
	}

}
